Skip to content

一、代码流程步骤:

https://docs.espressif.com/projects/esp-techpedia/zh_CN/latest/esp-friends/index.html

二、CPU中断类型(UART中断补充内容)

GPIO 中断只告诉你“哪个引脚触发了中断”,不告诉你“发生了什么类型事件”或“数据内容”——因为它没有数据通道,只有电平/边沿变化。所以只需传递 gpio_num 即可,无需复杂事件结构体。


gpio_install_isr_service() + gpio_isr_handler_add() 的工作原理
当你使用这套组合时,驱动层会安装一个全局 GPIO ISR,这个全局 ISR 负责接收所有 GPIO 中断信号,然后自动判断是哪个引脚触发了中断,再调用你通过 gpio_isr_handler_add() 为该引脚绑定的对应回调函数。


gpio_isr_handler_add() 的分发逻辑是:哪个引脚触发了中断,就调用哪个引脚绑定的 ISR。所以gpio_isr_handler_add()必须绑定好对应的引脚,比如 就算是GPIO 2 中断触发,但是gpio_isr_handler_add()并没有绑定GPIO 2 引脚,那么也没有任何的GPIO 2 的回调函数需要执行。

外内部中断类型

示波器电平变化及上升下降沿

ESP32_s3中断类型及数量

三、为什么ESP-IDF中UART 有 uart_event_t,而 GPIO 没有?

特性UART 中断GPIO 中断
是否有数据流?✅ 有(接收到字节流)❌ 无(只有电平跳变)
是否需要缓冲区?✅ 需要环形缓冲区存储数据❌ 不需要
事件类型多样吗?✅ 有:数据到达、超时、错误、发送完成等❌ 只有一种:“引脚电平变了”
驱动是否封装事件?✅ ESP-IDF UART 驱动内部维护事件队列❌ GPIO 驱动只负责触发 ISR,不管理事件

四、GPIO 中断的核心思想

GPIO 中断的作用是:

当某个 GPIO 引脚的电平发生指定变化(如下降沿、上升沿、低电平等)时,CPU 立即暂停当前任务,执行一个简短的中断服务程序(ISR),ISR中断服务程序必须极快处理,ISR发送不能长时间占用CPU资源,在微秒级处理完成,ISR从中断中向队列发送数据(注意:使用 xQueueSendFromISR,必须使用 IRAM_ATTR(因为 ISR 在 RAM 中执行),且只能调用带 _from_isr 后缀的 FreeRTOS 函数),CPU处理完ISR中断服务程序后,返回继续原来的工作。

由于 ISR 必须非常快且不能阻塞,我们通常:

  1. 在 ISR 中 只记录“哪个引脚触发了中断” ;
  2. 将该信息 发送到 FreeRTOS 队列;
  3. 在一个普通任务(Task)中 从队列读取并做实际处理(如控制 LED、打印日志、网络通信等)。

📌 ESP-IDF 不提供 gpio_event_t ,你需要自己决定队列里传什么,直接传当前出发了中断的引脚号Number(通常是 uint32_t gpio_num )。

GPIO中断及CPU响应并执行ISR流程1GPIO中断及CPU响应并执行ISR流程2

乐鑫官方AI技术开发助手_ESP-IDF开发问题

五、GPIO 中断开发 checklist

c
/*
 *✅ 包含头文件
 *✅ 定义队列句柄
 *✅ 编写 IRAM_ATTR 的 ISR 函数 (制定xQueueSendFromISR() 发送出去的数据的结构长什么样子,发送什么,单数据无需结构体)
 *✅ 编写任务函数(用 xQueueReceive)
 *✅ 配置 GPIO(输入 + 中断类型 + 上下拉)
 *✅ 调用 gpio_install_isr_service()
 *✅ 调用 gpio_isr_handler_add()
 *✅ 创建任务
 */

六、代码实现

GPIO中断代码编写流程步骤2

c
/*
 * Copyright 2026 MorkenMooooo
 * Author: Mooooo(门主)
 * Platform: Bilibili & Douyin & WeChatOfficial @ 门主引擎 | Mooooo
 * Contact: [email protected]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_timer.h"  // 提供 esp_timer_get_time()

// ================= 全局定义 =================
#define TAG "gpio_intr"

#define BUTTON_PIN   GPIO_NUM_21
#define LED_PIN      GPIO_NUM_2
#define ESP_INTR_FLAG_DEFAULT 0

// 队列句柄(必须在 ISR 前声明)
QueueHandle_t gpio_event_queue = NULL;

// 自定义ISR事件结构体(需要传递什么参数,结构体可以一次性传输)
typedef struct {
    uint32_t gpio_num;
    uint64_t timestamp_us;
} gpio_event_t;

// ================= ISR 函数(去掉 static,保留 IRAM_ATTR)=================
void IRAM_ATTR gpio_isr_handler(void* arg)
{
    uint32_t gpio_num = (uint32_t)arg;
    gpio_event_t event = {
        .gpio_num = gpio_num,
        .timestamp_us = esp_timer_get_time()
    };

    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(gpio_event_queue, &event, &xHigherPriorityTaskWoken);
    if (xHigherPriorityTaskWoken) {
        portYIELD_FROM_ISR();
    }
}

// ================= 任务函数 =================
void gpio_task(void* arg)
{
    gpio_event_t event;
    for (;;) {
        if (xQueueReceive(gpio_event_queue, &event, portMAX_DELAY)) {
            // === 软件消抖:等待 30ms 后再次检查电平 ===
            vTaskDelay(pdMS_TO_TICKS(30));  // 关键:消抖延时

            // 重新读取当前电平(必须是低电平才认为是有效按下)
            if (gpio_get_level(BUTTON_PIN) == 0) {
                ESP_LOGI(TAG, "Button pressed (debounced)");

                // 翻转 LED
                int led_state = gpio_get_level(LED_PIN);
                gpio_set_level(LED_PIN, !led_state);
            } else {
                ESP_LOGI(TAG, "False trigger (noise or bounce)");
            }
        }
    }
}

// ================= 主函数 =================
void app_main(void)
{
    // 1. 配置 LED 引脚(输出)
    gpio_reset_pin(LED_PIN);
    gpio_set_direction(LED_PIN, GPIO_MODE_INPUT_OUTPUT);
    gpio_set_level(LED_PIN, 0); // 初始关闭

    // 2. 配置按钮引脚(输入 + 上拉 + 下降沿中断)
    gpio_config_t button_config = {
        .pin_bit_mask = (1ULL << BUTTON_PIN),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,      // 按键接 GND,启用上拉
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_NEGEDGE         // 按下时产生下降沿
    };
    gpio_config(&button_config);

    // 3. 创建队列
    gpio_event_queue = xQueueCreate(10, sizeof(gpio_event_t));
    if (!gpio_event_queue) {
        ESP_LOGE(TAG, "Failed to create queue");
        return;
    }

    // 4. 安装 ISR 服务
    gpio_install_isr_service(0);

    // 5. 注册 ISR 到按钮引脚
    gpio_isr_handler_add(BUTTON_PIN, gpio_isr_handler, (void*)BUTTON_PIN);

    // 6. 创建任务
    xTaskCreate(gpio_task, "gpio_task", 2048, NULL, 10, NULL);

    ESP_LOGI(TAG, "GPIO interrupt example ready. Press button on GPIO %d", BUTTON_PIN);
}

七、代码解释

问题一:为什么 ISR 要加 IRAM_ATTR

c
void IRAM_ATTR gpio_isr_handler(void* arg)
{
    uint32_t gpio_num = (uint32_t)arg;
    gpio_event_t event = {
        .gpio_num = gpio_num,
        .timestamp_us = esp_timer_get_time()
    };

IRAM_ATTR 是什么?

在 ESP32(以及大多数嵌入式系统)中,内存分为两类:

  • Flash (XIP - eXecute In Place):代码默认存储在这里。CPU 访问 Flash 速度较慢,且在某些情况下(如 Wi-Fi/BT 操作时)可能被暂停访问。
  • IRAM (Instruction RAM):高速内部 SRAM,专门用于存放必须快速、可靠执行的代码。

IRAM_ATTR 是一个宏,它告诉编译器:“请把这个函数编译后放到 IRAM 里,而不是 Flash 里!”

c
// 展开后类似这样(具体由 xtensa 编译器定义)
#define IRAM_ATTR __attribute__((section(".iram1")))

所以:所有 ISR 函数必须用 IRAM_ATTR 标记。

问题二、BaseType_t xHigherPriorityTaskWoken = pdFALSE; 干什么用的?

c
 BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(gpio_event_queue, &event, &xHigherPriorityTaskWoken);
    if (xHigherPriorityTaskWoken) {
        portYIELD_FROM_ISR();
    }

BaseType_t xHigherPriorityTaskWoken = pdFALSE;

  • BaseType_t:FreeRTOS 的标准布尔/状态类型(通常是 int)。
  • pdFALSE:FreeRTOS 定义的“假”值(通常是 0)。
  • 作用:初始化一个标志变量,用于告诉 FreeRTOS:“目前还没有更高优先级的任务被唤醒”。

✅ 这是一个 输出参数(out-parameter),稍后会被 xQueueSendFromISR 修改。


2. xQueueSendFromISR(gpio_event_queue, &event, &xHigherPriorityTaskWoken);

这是关键!

  • xQueueSendFromISR:FreeRTOS 提供的 ISR 安全版队列发送函数。
  • 第三个参数 &xHigherPriorityTaskWoken 是一个 指针,函数会通过它返回一个信息:“这次发送操作是否导致一个更高优先级的任务从阻塞态变为就绪态?”
📌 举个例子:

假设你的系统中有两个任务:

  • Task_A(低优先级):正在运行
  • Task_B(高优先级):正在 xQueueReceive() 上等待 gpio_event_queue 的数据

当 ISR 调用 xQueueSendFromISR 发送事件时:

  • Task_B 立刻被唤醒(因为队列有数据了)
  • 由于 Task_B 优先级 > Task_A,FreeRTOS 会把 *xHigherPriorityTaskWoken 设为 pdTRUE

3. if (xHigherPriorityTaskWoken) { portYIELD_FROM_ISR(); }

  • portYIELD_FROM_ISR():这是一个宏,作用是 触发一次上下文切换(Context Switch)。
  • 它只在 中断上下文 中调用。
📌 继续上面的例子:
  • 如果 xHigherPriorityTaskWoken == pdTRUE,说明有更高优先级任务就绪。
  • 调用 portYIELD_FROM_ISR() 后,CPU 会在 退出 ISR 后立即切换到 Task_B,而不是回到原来的 Task_A。
  • 这样就实现了 “中断 → 高优先级任务”的零延迟响应。

💡 如果没有这一步,系统会先返回到低优先级的 Task_A,等它下次被调度器切走后,Task_B 才能运行——这会引入不必要的延迟!


不加portYIELD_FROM_ISR加了portYIELD_FROM_ISR

  • 不加 portYIELD_FROM_ISR: 你收到消息,但继续开完会再去 → CEO 等得生气。
  • 加了 portYIELD_FROM_ISR: 你收到消息,立刻中断会议,直奔 CEO 办公室 → 响应及时,老板满意。

ISR 就是那个“传消息的人”,而 portYIELD_FROM_ISR 就是“允许你立刻离场”的特权。

觉醒,然后燎原。 © 2026 门主引擎